﻿// ===============================================================================
// Injection.cs
// EOS Web Framework
// ===============================================================================
// Copyright (c) EOS Development Group. All rights reserved.
// ===============================================================================

namespace EOS.Framework.Web.Mvc
{
    using System;
    using System.Reflection;
    using EOS.Framework.Tools;
    
    /// <summary>
    /// Helper class for value injection.
    /// </summary>
    public static class Injection
    {
        /// <summary>
        /// Injects the value to the specified target.
        /// </summary>
        /// <param name="target">The target, where the value should injected to. Cannot be null (Nothing in Visual Basic).</param>
        /// <param name="name">The name of the field or property. Cannot be null (Nothing in Visual Basic).</param>
        /// <param name="value">The value to inject. Cannot be null (Nothing in Visual Basic).</param>
        /// <exception cref="ArgumentNullException">
        /// 	<para><paramref name="target"/> is null (Nothing in Visual Basic).</para>
        /// 	<para>- or -</para>
        /// 	<para><paramref name="name"/> is null (Nothing in Visual Basic).</para>
        /// 	<para>- or -</para>
        /// 	<para><paramref name="value"/> is null (Nothing in Visual Basic).</para>
        /// </exception>
        /// <exception cref="ArgumentException"><paramref name="name"/> is a string with length 0 or contains only blanks.</exception>
        /// <remarks>
        /// The method tries to inject the specified value to the target object. There are two possibilities, when
        /// the value can be injected.
        ///     <para>1. There is a field with the same name like the specified name in camel case, which has
        /// an assigned <see cref="InjectAttribute"/></para>
        /// 	<para>2. There is a property with the same name like the specified name in pascal case, which has
        /// an assigned <see cref="InjectAttribute"/></para>
        /// The method tries to set the value to the property first. If there is no matching property found, it tries to set the value to a field.
        /// </remarks>
        /// <example>
        /// Using the function is very easy. We use the following class:
        /// <code>
        /// public class Foo
        /// {
        ///     // Define a field which value will be injected.
        ///     [Inject]
        ///     private string bar;
        ///     
        ///     // Define a property which value will be injected.
        ///     [Inject]
        ///     public int Foobar
        ///     {
        ///         get; set; 
        ///     }
        /// }
        /// </code>
        /// Now, it is possible to inject values like this:
        /// <code>
        /// Foo foo = new Foo();
        /// 
        /// Injection.InjectValue(foo, "bar", "MyInjectedString"),
        /// Injection.InjectValue(foo, "foobar", 123);
        /// </code>
        /// </example>
        public static void InjectValue(object target, string name, object value)
        {
            InjectValue(target, name, value, false);
        }

        /// <summary>
        /// Injects the value to the specified target.
        /// </summary>
        /// <param name="target">The target, where the value should injected to. Cannot be null (Nothing in Visual Basic).</param>
        /// <param name="name">The name of the field or property. Cannot be null (Nothing in Visual Basic).</param>
        /// <param name="value">The value to inject. Cannot be null (Nothing in Visual Basic).</param>
        /// <param name="noAttribute">if set to <c>true</c> the field or property doesnt needs an <see cref="InjectAttribute"/>.</param>
        /// <exception cref="ArgumentNullException">
        /// 	<para><paramref name="target"/> is null (Nothing in Visual Basic).</para>
        /// 	<para>- or -</para>
        /// 	<para><paramref name="name"/> is null (Nothing in Visual Basic).</para>
        /// 	<para>- or -</para>
        /// 	<para><paramref name="value"/> is null (Nothing in Visual Basic).</para>
        /// </exception>
        /// <exception cref="ArgumentException"><paramref name="name"/> is a string with length 0 or contains only blanks.</exception>        
        /// <remarks>
        /// The method tries to inject the specified value to the target object. There are two possibilities, when
        /// the value can be injected.
        ///     <para>1. There is a field with the same name like the specified name in camel case, which has
        /// an assigned <see cref="InjectAttribute"/></para>
        /// 	<para>2. There is a property with the same name like the specified name in pascal case, which has
        /// an assigned <see cref="InjectAttribute"/></para>
        /// The method tries to set the value to the property first. If there is no matching property found, it tries to set the value to a field.
        /// </remarks>        
        /// <example>
        /// Using the function is very easy. We use the following class:
        /// <code>
        /// public class Foo
        /// {
        ///     // Define a field which value will be injected.
        ///     [Inject]
        ///     private string bar;
        ///     
        ///     // Define a property which value will be injected.
        ///     [Inject]
        ///     public int Foobar
        ///     {
        ///         get; set; 
        ///     }
        /// }
        /// </code>
        /// Now, it is possible to inject values like this:
        /// <code>
        /// Foo foo = new Foo();
        /// 
        /// Injection.InjectValue(foo, "bar", "MyInjectedString"),
        /// Injection.InjectValue(foo, "foobar", 123);
        /// </code>
        /// </example>
        public static void InjectValue(object target, string name, object value, bool noAttribute)
        {
            if (target == null)
            {
                throw new ArgumentNullException("target");
            }

            if (name == null)
            {
                throw new ArgumentNullException("name");
            }

            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            if (StringHelper.IsEmptyOrBlanksOnly(name))
            {
                throw new ArgumentException(Properties.Resources.EmptyStringParameter, "name");
            }

            PropertyInfo property = target.GetType().GetProperty(char.ToUpper(name[0]) + name.Substring(1).ToLower());
            if (property != null)
            {
                SetValueToProperty(property, target, value, noAttribute);
            }

            FieldInfo field = target.GetType().GetField(name.ToLower(), BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
            if (field != null)
            {
                SetValueToField(field, target, value, noAttribute);
            }
        }

        /// <summary>
        /// Sets the value to the specified field.
        /// </summary>
        /// <param name="field">The field to set the value to.</param>
        /// <param name="target">The target object.</param>
        /// <param name="value">The value to set to the field.</param>        
        /// <exception cref="ArgumentNullException">
        ///     <para><paramref name="field"/> is null (Nothing in Visual Basic).</para>
        ///     <para>- or -</para>
        ///     <para><paramref name="target"/> is null (Nothing in Visual Basic).</para>
        ///     <para>- or -</para>
        ///     <para><paramref name="value"/> is null (Nothing in Visual Basic).</para>
        /// </exception>
        /// <remarks>The value can only be set to the field, if the field has an <see cref="InjectAttribute"/>.</remarks>
        public static void SetValueToField(FieldInfo field, object target, object value)
        {
            SetValueToField(field, target, value);
        }

        /// <summary>
        /// Sets the value to the specified field.
        /// </summary>
        /// <param name="field">The field to set the value to.</param>
        /// <param name="target">The target object.</param>
        /// <param name="value">The value to set to the field.</param>
        /// <param name="noAttribute">if set to <c>true</c> the the field doesnt needs an <see cref="InjectAttribute"/>.</param>
        /// <exception cref="ArgumentNullException">
        /// 	<para><paramref name="field"/> is null (Nothing in Visual Basic).</para>
        /// 	<para>- or -</para>
        /// 	<para><paramref name="target"/> is null (Nothing in Visual Basic).</para>
        /// 	<para>- or -</para>
        /// 	<para><paramref name="value"/> is null (Nothing in Visual Basic).</para>
        /// </exception>
        /// <remarks>
        /// Depending on the <paramref name="noAttribute"/> parameter, the value can only be set to the field,
        /// if the field has an <see cref="InjectAttribute"/>.
        /// </remarks>
        public static void SetValueToField(FieldInfo field, object target, object value, bool noAttribute)
        {
            if (target == null)
            {
                throw new ArgumentNullException("target");
            }

            if (field == null)
            {
                throw new ArgumentNullException("field");
            }

            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            object[] injectAttributes = field.GetCustomAttributes(typeof(InjectAttribute), true);

            if (noAttribute)
            {
                field.SetValue(target, ChangeType(field.FieldType, value));
                return;
            }

            if (injectAttributes.Length == 1)
            {
                field.SetValue(target, ChangeType(field.FieldType, value));
            }
        }

        /// <summary>
        /// Sets the value to the specified property.
        /// </summary>
        /// <param name="property">The field to set the value to.</param>
        /// <param name="target">The target object.</param>
        /// <param name="value">The value to set to the property.</param>        
        /// <exception cref="ArgumentNullException">
        ///     <para><paramref name="property"/> is null (Nothing in Visual Basic).</para>
        ///     <para>- or -</para>
        ///     <para><paramref name="target"/> is null (Nothing in Visual Basic).</para>
        ///     <para>- or -</para>
        ///     <para><paramref name="value"/> is null (Nothing in Visual Basic).</para>
        /// </exception>
        /// <remarks>The value can only be set to the field, if the field has an <see cref="InjectAttribute"/>.</remarks>
        public static void SetValueToProperty(PropertyInfo property, object target, object value)
        {
            SetValueToProperty(property, target, value, false);
        }

        /// <summary>
        /// Sets the value to the specified property.
        /// </summary>
        /// <param name="property">The property to set the value to.</param>
        /// <param name="target">The target object.</param>
        /// <param name="value">The value to set to the property.</param>        
        /// <param name="noAttribute">if set to <c>true</c> the the property doesnt needs an <see cref="InjectAttribute"/>.</param>
        /// <exception cref="ArgumentNullException">
        ///     <para><paramref name="property"/> is null (Nothing in Visual Basic).</para>
        ///     <para>- or -</para>
        ///     <para><paramref name="target"/> is null (Nothing in Visual Basic).</para>
        ///     <para>- or -</para>
        ///     <para><paramref name="value"/> is null (Nothing in Visual Basic).</para>
        /// </exception>        
        /// <remarks>
        /// Depending on the <paramref name="noAttribute"/> parameter, the value can only be set to the property,
        /// if the property has an <see cref="InjectAttribute"/>.
        /// </remarks>
        public static void SetValueToProperty(PropertyInfo property, object target, object value, bool noAttribute)
        {
            if (target == null)
            {
                throw new ArgumentNullException("target");
            }

            if (property == null)
            {
                throw new ArgumentNullException("property");
            }

            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            object[] injectAttributes = property.GetCustomAttributes(typeof(InjectAttribute), true);

            if (noAttribute)
            {
                property.SetValue(target, ChangeType(property.PropertyType, value), null);
                return;
            }

            if (injectAttributes.Length == 1 || noAttribute)
            {
                property.SetValue(target, ChangeType(property.PropertyType, value), null);
            }
        }

        /// <summary>
        /// Converts the value to the specified type.
        /// </summary>
        /// <param name="targetType">Type, where the object should converted to. Cannot be null.</param>
        /// <param name="source">The source value. Cannot be null.</param>
        /// <returns>The converted object.</returns>
        /// <exception cref="ArgumentNullException">
        /// 	<para><paramref name="targetType"/> is null (Nothing in Visual Basic).</para>
        /// 	<para>- or -</para>
        /// 	<para><paramref name="source"/> is null (Nothing in Visual Basic).</para>
        /// </exception>
        /// <exception cref="InvalidCastException">Source value cannot be casted to the specified type.</exception>
        public static object ChangeType(Type targetType, object source)
        {
            Type sourceType = source.GetType();

            if (sourceType.Equals(targetType) ||
                sourceType.IsSubclassOf(targetType) ||
                sourceType.IsInstanceOfType(targetType) ||
                targetType.IsAssignableFrom(source.GetType()))
            {
                return source;
            }
            else if (targetType.IsEnum && source is string)
            {
                return Enum.Parse(targetType, source.ToString());
            }
            else if (source is IConvertible)
            {
                return Convert.ChangeType(source, targetType);
            }
            else
            {
                throw new InvalidCastException();
            }
        }
    }
}
